# El modulo Graphics contiene las diferentes herramientas que se utilizan para abstraer y encapsular los diferentes aspectos graficos del juego.

import os, time, operator, pygame, math

from Util import Enum, Subclasser, Factory
from pygame import image
from copy import copy
from random import choice

_loaders = { "default":None }

def register_loader( dir, loader_name="default", type="sprite" ):
	'''
	This method is given so anyone who wants to use this module as to directly load sprites and other types of graphics
	resources, can create and manage a group of loaders and call any load operation directly to the module, via the load
	method. 
	
	Argument Keywords:
	dir -- directory for the loader.
	loader_name -- name of the loader. If no name is given, it will set to "default" overwriting any other default loader.
	type -- type of loader (sprite, image or attachs).
	'''
	global _loaders
	_loaders[loader_name] = GraphicLoaderFactory.new( type, dir )
	
def load( resource, loader_name='default', dir_tree=[], **extra_parameters ):
	'''
	This method redirects the loading to a registered loader, passing the arguments given to this method to the loader, 
	so it handles the operation.
	
	Argument Keywords:
	resource -- resource to be laoded (action name, weapon name, etc)
	loader_name -- name of the loader to be used. If no loader name is given, the default loader will be used.
	dir_tree -- directory tree from where to load the resource, relative of the base dir of the loader.
	**extra_parameters -- if the graphic resource to be loaded is an sprite animation, here is where you can put
	the animation parameters, as the anchor, delay and/or scale.	
	'''
	#variable to contain the graphic resource
	graphic = None
	
	#retrieve the loader
	global _loaders
	loader = _loaders[loader_name]
	
	#load the resource
	if loader:
		graphic = loader.load( resource, dir_tree, **extra_parameters )
		
	return graphic	
	
class SpriteAnimation( object ):
	'''
	This class defines an animation, wichever the animation would be. It takes a list of frames given to it as a constructor
	argument, and later on it can play them in secuence, wich order is given by frame list indexes.
	'''
	ANCHOR = Enum( [ 'center', 'top', 'bot', 'left', 'right', 'leftop', 'leftbot', 'righttop', 'rightbot' ] )

	def __init__ ( self, frames, playable, anchor, delay ):
		'''
		Instantiate an sprite, that could be used as a base sprite (a generator sprite) or a playable sprite.
		
		Keywords:
		frames -- Animations frames on a list.
		playable -- Indicates if this animation is playable or not. Non playable animation is generally used to generate
		other sprites that share the same frames and can be played.
		anchor -- Animation anchor for the frames.		
		delay -- Delay between frames. Denoted in milliseconds.   
		'''
		#invalid anchor
		if anchor not in self.ANCHOR:
			anchor = "center"

		#attributes initialization
		self._playable = playable				#animation playable
		self._anchor = anchor					#animation anchor
		self._delay = delay * .001				#animation delay (converted to milliseconds)
		self._frames = frames					#frame list
		self._current_frame = 0					#current frame
		self._time_last = 0						#last call to render function
		self._startpoint = 0					#first frame
		self._breakpoint = len( frames ) - 1 	#last frame

		self.playing = False					#playing state
		self.reversed = False					#reversed animation

		self._calculate_size()					#animation overall size
		self._calculate_coords()  				#frames' coords list
		
		#create the attachs dict
		self._attach_dict = {}
		
		for i in range( 0, len( self._frames ) ):
			self._attach_dict[i] = { 'front':[], 'back':[] }
			
	def _calculate_size( self ):
		''' Calculate the overall size of the animation using the biggest surface loaded. '''
		may_x = 0
		may_y = 0

		for frame in self._frames:
			size = frame.get_size()

			may_x = max( may_x, size[0] )
			may_y = max( may_y, size[1] )

		self.size = ( may_x, may_y )

	def _calculate_coords( self ):
		'''Calculate the coords for each frame to be consistent with the animation anchor.'''
		self.coords = []

		for i in range( 0, len( self._frames ) ):
			self.coords.append( self._calculate_frame_coords( i ) )

	def _calculate_frame_coords( self, frame_num ):
		'''
		Calculate the coords of a frame to be consistent with the animation anchor.
		
		Keywords:
		frame_num -- Number of the frame wich coords will be calculated.
		'''
		if self._anchor == self.ANCHOR.center:
			x_offset = ( self.size[0] - self._frames[frame_num].get_width() ) / 2
			y_offset = ( self.size[1] - self._frames[frame_num].get_height() ) / 2
		elif self._anchor == self.ANCHOR.bot:
			x_offset = ( self.size[0] - self._frames[frame_num].get_width() ) / 2
			y_offset = self.size[1] - self._frames[frame_num].get_height()
		elif self._anchor == self.ANCHOR.top:
			x_offset = ( self.size[0] - self._frames[frame_num].get_width() ) / 2
			y_offset = 0
		elif self._anchor == self.ANCHOR.left:
			x_offset = 0
			y_offset = ( self.size[1] - self._frames[frame_num].get_height() ) / 2
		elif self._anchor == self.ANCHOR.right:
			x_offset = self.size[0] - self._frames[frame_num].get_width()
			y_offset = ( self.size[1] - self._frames[frame_num].get_height() ) / 2
		elif self._anchor == self.ANCHOR.leftbot:
			x_offset = 0
			y_offset = y_offset = ( self.size[1] - self._frames[frame_num].get_height() )
		elif self._anchor == self.ANCHOR.rightbot:
			x_offset = self.size[0] - self._frames[frame_num].get_width()
			y_offset = self.size[1] - self._frames[frame_num].get_height()
		elif self._anchor == self.ANCHOR.lefttop:
			x_offset = 0
			y_offset = 0
		elif self._anchor == self.ANCHOR.righttop:
			x_offset = self.size[0] - self._frames[frame_num].get_width()
			y_offset = 0

		return ( x_offset, y_offset )

	def change_anchor( self, anchor ):
		self._anchor = anchor
		self._calculate_coords()

	def play( self ):
		'''Initiates the animation'''
		self.playing = True
		self._time_last = time.time()

	def stop( self ):
		'''Stops the animation'''
		self.playing = False

	def seek( self, frame ):
		'''Seek for an specific frame in the animation'''
		self._current_frame = frame

		if self._current_frame < self._startpoint:
			self._current_frame = self._breakpoint
		if self._current_frame > self._breakpoint:
			self._current_frame = self._startpoint
			
	def next_frame( self ):
		'''Advance the animation to the next frame.'''
		self.seek( self._current_frame + 1 )
		
	def previous_frame( self ):
		'''Retrocede the animation to the previous frame.'''
		self.seek( self._current_frame - 1 )

	def reverse( self ):
		'''Reverses the animation.'''
		self.reversed = not self.reversed

	def get_height( self ):
		'''Returns the height of the animation.'''
		return self.size[1]

	def get_width( self ):
		'''Returns the width of the animation.'''
		return self.size[0]

	def get_size( self ):
		'''Returns the animation's size'''
		return self.size
	
	def get_rect( self, topleft ):
		'''Returns the bounding rect of the SpriteAnimation.'''
		return self._frames[self._current_frame].get_rect( topleft=topleft )
	
	def get_delay( self ):
		''' Returns the animation's delay. '''
		return self._delay
	
	def get_anchor( self ):
		'''Returns the animation's anchor. '''
		return self._anchor
	
	def get_current_frame( self ):
		''' Returns the animation's current frame '''
		return self._current_frame
	
	def is_playable( self ):
		''' Indicates if the sprite is playable. '''
		return self._playable

	def length( self ):
		'''Returns the animation lenght in frames'''
		return len( self._frames )

	def reset( self ):
		'''Resets the animation to frame 0 and not reversed'''
		self._current_frame = 0
		self._time_last = time.time()
		self.reversed = False
		
	def attach( self, image, frame, pos, front=True, ):
		'''
		Attachs an image in a given frame and position within the frame.
		
		Argument Keywords:
		image -- Image to attach.
		frame -- Frame of the animation where the image will be attached.
		pos -- Position in where it will be attached.
		front -- Indicates wheter the image will be render in front or behind the animation. 
		'''
		if frame > self.length() or frame < 0:
			return		
		elif front:
			self._attach_dict[frame]['front'].append( ( image, pos ) )
		else:
			self._attach_dict[frame]['back'].append( ( image, pos ) )
			
	def deattach( self, image ):
		'''
		Deattachs an image from all the frames where it's attached.
		
		Argument Keywords:
		image -- Image to deattach.
		'''
		for frame in self._attach_dict.values():
			for perspective in frame.values():
				for i in xrange( 0, len( perspective ) ):
					if image in perspective[i]:
						del( perspective[i] )
						
	def render( self, screen, coords ):
		'''
		Renders the animation in the screen giveng the current time and the delay between frames. 
		
		Keywords:
		screen -- Surface where the animation frame will be rendered.
		coords -- Coordinates of the left-top corner of where the animation wants to be rendered on the surface.
		'''
		if self._playable and len( self._frames ) > 0:
			if self.playing and len( self._frames ) > 1:
				if time.time() - self._time_last > self._delay:
					if self.reversed:
						self.previous_frame()
					else:
						self.next_frame()

					self._time_last = time.time()

			#render all the back attachments
			for attach in self._attach_dict[self._current_frame]['back']:
				screen.blit( attach[0], tuple( map( operator.add, coords, attach[1] ) ) )
				
			#render the animation itself
			screen.blit( self._frames[self._current_frame], tuple( map( operator.add, coords, self.coords[self._current_frame] ) ) )
			
			#render all the front attachments
			for attach in self._attach_dict[self._current_frame]['front']:
				screen.blit( attach[0], tuple( map( operator.add, coords, attach[1] ) ) )
					
	def copy( self, playable=True, scale=None, copy_attachs=True ):
		'''
		Returns a copy of this sprite animation.
		
		Keywords:
		playable -- Indicates if the copy given should be playable.
		scale -- gives an scale for the copy.
		copy_attachs -- indicates if the copy should have the same attachs as the original.
		
		Note:
		Every copy of an animation share the same frames, unless it's an scaled copy, in wich case, new frames
		are created with the new scale.
		When a sprite has attachments, these are NOT copied to the new Sprite created.
		'''
		#if no scale is needed, a shallow copy is handled. Otherwise, frames are scalled and returned into a mixed
		#copy
		if not scale:
			new = SpriteAnimation( self._frames, playable, self._anchor, self._delay * 1000 )
			new._playable = playable	
			
			#copy attachs
			if copy_attachs:
				for i in xrange( 0, len( self._frames ) ):
					new._attach_dict[i]['front'] = copy( self._attach_dict[i]['front'] )
					new._attach_dict[i]['back'] = copy( self._attach_dict[i]['back'] )
			
		else:
			#scale the frames
			frames = self._frames[:]

			for i in range( 0, len( frames ) ):
				frames[i] = pygame.transform.smoothscale( frames[i], ( int( frames[i].get_width() * scale[0] ), int( frames[i].get_height() * scale[1] ) ) )

			#create the new sprite
			new = SpriteAnimation( frames, playable, self._anchor, self._delay * 1000 )	
			
			#TODO: copy scaled attachs	

		return new
	
class Texture( object ):
	''' Represents a flippable texture wich different flips can be accesed directly trought a list-like sintaxis. '''
	def __init__( self, image ):
		''' Creates a new texture from an image. '''
		self._flips = {}		#flips dictionary
		
		#generate and assign the flips to the dict
		self._flips[''] = image
		self._flips['h'] = pygame.transform.flip( image, True, False )
		self._flips['v'] = pygame.transform.flip( image, False, True )
		self._flips['hv'] = self._flips['vh'] = pygame.transform.flip( image, True, True )
						
	def random( self ):
		''' Returns a random flip of the texture. '''
		return choice( self._flips.values() )
	
	def get_flip( self, flip='' ):
		'''
		Returns the indicated flip of the texture. By default, return the unflipped image.
		
		Argument Keywords:
		flip -- key indicating the flip of the texture. 'h' is for horizontal flip, 'v' for vertical flip and
		either 'hv' or 'vh' for vertical AND horizontal flip.		
		'''
		return self._flips[flip]
	
	def __getitem__( self, key ):
		''' Identical to get_flip() but with list-like sintaxis. '''
		if key == 'r':
			return self.random()
		else:
			return self._flips[key]
	
	def __copy__( self ):
		''' Return a shallow copy of this texture (the images are shared). '''
		new = Texture.__new__( Texture )
		new._flips = self._flips.copy()
		
		return new

class GraphicLoader( object ):	
	'''
	This class is an interface to load any kind of graphic resource without the need of directly naming the images directories, 
	images names or	anything related to the "low level" activity of actually loading the sprites. 
	The class have methods for loading and retrieving graphic resources from a base directory.
	Plus, it mantains a cache graphics' index, so loading a graphic resource that was cached beforehand is a cheaper and 
	faster operation.
	
	'''	
	#loader type
	key = 'base'
	
	#metaclass for subclass tracking
	__metaclass__ = Subclasser

	def __init__( self, dir ):
		'''
		Initializes the loader to a given directory, and to load a certain type of graphic resources.
		
		Argument Keywords:
		dir -- directory from where the loading attemps will check for images.
		'''		
		self._dir = dir			#loader base directory
			
		self._cache = dict()	#graphic cache
		
	def clear_cache( self ):
		''' Clears the cache of loaded resources. '''
		self._cache.clear()		
		
	def load( self, *args, **kwargs ):
		raise RuntimeWarning( "Base loader doesn't implement a load function. Use an specific type of loader!" )

class ImageLoader( GraphicLoader ):
	''' Specific graphic loader in charge of loading simple images. '''
	
	#loader type
	key = 'image'
	
	def __init__( self, dir ):
		''' Initializes the loader to point to a given directory. '''
		super( ImageLoader, self ).__init__( dir )
		
	def load( self, resource, dir_tree=[], scale=None ):
		'''
		Loads and returns an image.
		
		Argument Keywords:
		resource -- graphic resource to load.
		dir_tree -- tree of directories given from the base directory defined in the initialization of the loader.
		scale -- indicates the scale of the image to be loaded.
		
		Note: the image scale parameter is only valid for the first time you load the image. Consecutive loadings of the same 
		resource will ignore this parameter and return a copy of the cached image.
		'''				
		#check for the resource on the cache dict. If not in there, load the resource and add the entry to the dict
		key = '-'.join( dir_tree ) + '-' + resource
		
		if key in self._cache.keys():
			graphic = self._cache[key]			
				
		else:		
			#form the directory from where to load
			resource_path = self._dir
		
			for i in xrange( 0, len( dir_tree ) ):
				resource_path = os.path.join( resource_path, dir_tree[i] )							
			
			#aux varible to know if the image should be flipped horizontally
			flip = False
				
			#for single file or texture: search the resource_path for the file and load it, adding alpha channel
			files = os.listdir( resource_path )
				
			match = [file for file in files if file.split( '.' )[0] == resource]
				
			#if the no file is matched, see if it's reversable
			if not len( match ):
				if 'right' in resource:
					resource = resource.replace( 'right', 'left' )
				elif 'left' in resource:
					resource = resource.replace( 'left', 'right' )
						
				match = [file for file in files if file.split( '.' )[0] == resource]
					
				flip = True
			
			#check if some file matched
			if not len( match ):
				raise RuntimeError( "Couldn't found " + resource + ' in ' + resource_path )	
					
			resource_path = os.path.join( resource_path, match[0] )
			graphic = image.load( resource_path ).convert_alpha()
					
			if flip:
				graphic = pygame.transform.flip( graphic, True, False )
					
			#scale if necesary
			if scale:
				graphic = pygame.transform.smoothscale( graphic , ( int( graphic.get_width() * scale[0] ), int( graphic.get_height() * scale[1] ) ) )
						
			#cache the image			
			self._cache[key] = graphic	
												
		return graphic		
	
class TextureLoader( ImageLoader ):
	''' Specific graphic loader in charge of loading Texture objects. '''
	#loader type
	key = 'texture'	
	
	def load( self, resource, dir_tree=[], scale=None ):
		'''
		Loads and returns a texture object.
		
		Argument Keywords:
		resource -- graphic resource to load.
		dir_tree -- tree of directories given from the base directory defined in the initialization of the loader.
		scale -- indicates the scale of the image to be loaded.
		
		Note: the texture scale parameter is only valid for the first time you load the image. Consecutive loadings of the same 
		resource will ignore this parameter and return a copy of the cached texture.
		'''				
		#check for the resource on the cache dict. If not in there, load the resource and add the entry to the dict
		key = '-'.join( dir_tree ) + '-' + resource
		
		if key in self._cache.keys():
			graphic = copy( self._cache[key] )		
				
		else:		
			#load the image to use for the texture
			image = super( TextureLoader, self ).load( resource, dir_tree, scale )
			
			#generate the texture
			graphic = Texture( image )
			
			#assign a copy to the cache
			self._cache[key] = copy( graphic )
		
		#return the texture	
		return graphic		
	
class SpriteLoader( GraphicLoader ):
	''' Specific graphic loader in charge of loading animated sprites. '''
	#loader type
	key = 'sprite'
	
	def __init__( self, dir ):
		'''
		Initializes the sprite loader.
		
		Argument keywords:
		dir -- base directory used to load the sprites.
		'''
		super( SpriteLoader, self ).__init__( dir )
		
	def load( self, resource, dir_tree=[], **extra_parameters ):
		'''
		Loads and returns an sprite. 
		
		Argument Keywords:
		resource -- graphic resource to load.
		dir_tree -- tree of directories given from the base directory defined in the initialization of the loader.
		**extra_parameters -- you can put the anchor, delay and scale parameters here.
		
		Note: the sprite parameters are only valid for the first time you load the sprite. Consecutive loadings of the same 
		resource will ignore this parameters and return a copy of the cached sprite.
		'''					
		#check for the resource on the cache dict. If not in there, load the resource and add the entry to the dict
		key = '-'.join( dir_tree ) + '-' + resource
		
		if key in self._cache.keys():
			graphic = self._cache[key].copy( copy_attachs=False )		
				
		else:		
			#form the directory from where to load
			resource_path = self._dir
		
			for i in xrange( 0, len( dir_tree ) ):
				resource_path = os.path.join( resource_path, dir_tree[i] )						

			#for sprite: get the attributes for the sprite and call the specific method to load sprites
			anchor = extra_parameters.get( 'anchor', 'center' )
			delay = extra_parameters.get( 'delay', 100 )
			scale = extra_parameters.get( 'scale', None )

			graphic = self._load_sprite_animation( resource_path, resource, anchor, delay, scale )
				
			self._cache[key] = graphic
				
			graphic = graphic.copy()			
			
		return graphic			
	
	def _load_sprite_animation( self, dir, action, anchor='center', delay=100, scale=None ):
		'''
		Loads the frames of an sprite and creates an SpriteAnimation class, not playable, that could be used as a base animation
		to create new ones that can be played later.
		This is the default path to create a SpriteAnimation, althougth you can create new methods that load the frames following
		a diferent pattern.
	
		Keyword Arguments:
		action -- The first part of the frame's file name.
		anchor -- Animation anchor, used to render the frames correctly. Center by default.
		delay -- Animation delay between frames given in ms. 100 ms by default.
		scale -- Scale that the animation should have. None by default.
	
		Note:
		For this method to function correctly, the format of the files that conform the animation should be the one showed next:
		file_name.frame_number.png
		'''
		#variable to indicate that the sprite my be an inverted sprite
		inverted = False
		#frame list
		frames = []

		#read the dir files
		files = os.listdir( dir )

		#get all files with the file_name and append them into the frame list 
		for file in files:
			if action == file.split( '.' ) [0]:
				frames.append( file )

		#if no file appended, try with the reflected frame( if left then right and viceversa)
		if len( frames ) == 0:
			#set inverted to true
			inverted = True

			#replace the name with the reflected name
			if 'right' in action:
				action = action.replace( 'right', 'left' )
			else:
				action = action.replace( 'left', 'right' )

			#check again	
			for file in files:
				if action == file.split( '.' ) [0]:
					frames.append( file )

		#if no frames where cache at the end, throw an exception
		if len( frames ) == 0:
			raise RuntimeError( "The sprite doesn't have any frames to load on the given directory." )

		#order the frames
		frames.sort( key=lambda frame: int( frame.split( '.' )[1] ) )

		#load the images
		for i in range( 0, len( frames ) ):
			#if file is reflected, flip the images horizontally
			if inverted:
				frames[i] = pygame.transform.flip( image.load( os.path.join( dir, frames[i] ) ), True, False )
			else:
				frames[i] = image.load( os.path.join( dir, frames[i] ) )

		#scale if necesary
		if scale:
			for i in range( 0, len( frames ) ):
				frames[i] = pygame.transform.smoothscale( frames[i], ( int( frames[i].get_width() * scale[0] ), int( frames[i].get_height() * scale[1] ) ) )

		#create and return the sprite animation
		return SpriteAnimation( frames, False, anchor, delay )
		
class GraphicLoaderFactory( Factory ):	
	''' Factory for graphic loaders. '''
	#base class
	_BASE_CLASS = GraphicLoader
	
	@classmethod
	def new( cls, type, dir ):
		'''
		Returns a new instance of the specified type of loader in the given directory.
		
		Argument Keywords:
		type -- type of loader.
		dir -- dir where the loader should be pointing to.
		'''
		return super( GraphicLoaderFactory, GraphicLoaderFactory ).new( type, dir )
		
		
class GraphicManager( object ):	
	'''
	This class is similar to GraphicLoader in the sense that it abstract the low level dinamics into the graphics managing. 
	But	the function of this class is quite different: it's on charge of attaching things to sprites and  generating changes 
	on graphics resources (e.g. palette changing).
	To use this class, you need to already have a loaded graphic resource that will be required frequently as a method parameter.
	'''
	#TODO: implement the class
	#NOTE: this should be a static class
	#Posible methods:
	#				change_alpha -> copy of the surface/sprite with a given alpha
	#				change_pallette -> copy of the surface/sprite with a given pallette
		
	@staticmethod
	def shear( surface, xshear_angle=0, yshear_angle=0 ):
		'''
		Return a new surface containing the original image sheared by a given angle, horizontally and vertically.
		
		Argument Keywords:
		xshear_angle -- shearing angle in the horizontal direction. 0 by default.
		yshear_angle -- sheargin angle in the vertical direction. 0 by default.
		'''
		Surface = pygame.Surface
		PixelArray = pygame.PixelArray
				
		#calculate the shearing rect pendant
		x_tangent = math.tan( math.radians( abs( xshear_angle ) ) )
		y_tangent = math.tan( math.radians( abs( yshear_angle ) ) )
		
		#calculate the shearing distances for files and columns
		width = surface.get_width()
		height = surface.get_height()
		
		xshear_distances = []
		yshear_distances = []
		
		for column in xrange( 0, height ):
			xshear_distances.append( int( column * x_tangent ) )
			
		for row in xrange ( 0, width ):
			yshear_distances.append( int( row * y_tangent ) )
			
		#create the surface where the shearing will be done
		width += xshear_distances[-1] 
		height += yshear_distances[-1] 
		
		shear_surface = Surface( ( width, height ), pygame.SRCALPHA, 32 )
		
		
		#create the pixel arrays to wrap over the surfaces
		original = PixelArray( surface )
		shear = PixelArray( shear_surface )
		
		#reverse the dist_lists if the angle is negative
		if xshear_angle < 0:
			xshear_distances.reverse()
		if yshear_angle < 0:
			yshear_distances.reverse()
		
		#do the shearing
		for row_number, row in enumerate( original ):
			for column_number, column in enumerate( row ):
				yindex = row_number + xshear_distances[column_number]
				xindex = column_number + yshear_distances[row_number]
				shear[yindex][xindex] = column
						
		#hack for negative - positive combinations
		if cmp( 0, xshear_angle ) + cmp( 0, yshear_angle ) == 0:
			shear = None
			shear_surface.blit( shear_surface, ( 1, 0 ) )
								
		#return the sheared surface
		return shear_surface
